反覆器讓我們取得聚合中的所有元素而不必暴露內部的實作細節。
假設你擁有很多電子書,但每本書的格式都不同,有些是 PDF,有些是 txt 檔,有些是網路上的連載文章。由於每種格式都需要不同的工具來閱讀,讓你無法隨時隨地享受閱讀樂趣。
我們可以透過電子書閱讀器來統一這些書籍的閱讀方式。閱讀器就像反覆器模式一樣,提供一個統一的閱讀介面,不論電子書的格式如何,你都可以透過相同的方式瀏覽頁面,例如使用「上一頁」、「下一頁」的按鈕切換頁面。有了閱讀器,你不必再為不同格式的電子書而煩惱,只需專心享受閱讀。
想像我們正在開發一款音樂播放器,它能夠支援 Apple Music 和 Spotify 這兩大音樂串流平台,並且允許用戶自由混搭不同平台的音樂清單。假設兩個平台的音樂清單格式不同,我們可以使用反覆器模式來包裝清單,並使用同一套邏輯來處理它們。
首先,我們來定義反覆器和聚合介面。Iterator
介面負責提供遍歷項目的方法。Iterable
介面則定義如何建立反覆器,讓我們透過它來遍歷集合中的項目。
interface Iterator<T = unknown> {
next(): T | undefined;
hasNext(): boolean;
}
interface Iterable {
createIterator(): Iterator;
}
接著,定義 Apple Music 歌單和對應的反覆器。反覆器會依序從 playlist
陣列中取出歌曲,並記錄目前的位置,讓我們可以按照順序播放歌曲。
class AppleMusicPlaylistIterator implements Iterator {
private playlist: AppleMusicTrack[];
private position: number;
constructor(playlist: AppleMusicTrack[]) {
this.playlist = playlist;
this.position = 0;
}
next() {
const track = this.playlist[this.position];
this.position += 1;
return track;
}
hasNext() {
return this.position + 1 <= this.playlist.length;
}
}
class AppleMusicPlaylist implements Iterable {
private playlist: AppleMusicTrack[];
constructor() {
this.playlist = [];
this.addTrack(new AppleMusicTrack("喜劇", "星野源"));
this.addTrack(new AppleMusicTrack("桃太郎", "水曜日のカンパネラ"));
this.addTrack(new AppleMusicTrack("エクレア", "岡崎体育"));
}
addTrack(track: AppleMusicTrack) {
this.playlist.push(track);
}
createIterator() {
return new AppleMusicPlaylistIterator(this.playlist);
}
}
定義 Spotify 的歌單與反覆器,這裡的歌單使用 Map
資料結構,反覆器會依序取出並移除已播放的歌曲。
class SpotifyPlaylistIterator {
private playlist: Map<string, SpotifyTrack>;
constructor(playlist: Map<string, SpotifyTrack>) {
this.playlist = playlist;
}
next() {
const trackName = Array.from(this.playlist.keys())[0];
const track = this.playlist.get(trackName);
this.playlist.delete(trackName);
return track;
}
hasNext() {
return this.playlist.size > 0;
}
}
class SpotifyPlaylist implements Iterable {
private playlist: Map<string, SpotifyTrack>;
constructor() {
this.playlist = new Map();
this.addTrack(new SpotifyTrack("旅路", "藤井風"));
this.addTrack(new SpotifyTrack("inside you", "milet"));
this.addTrack(new SpotifyTrack("HOPE", "TENDRE"));
}
addTrack(track: SpotifyTrack) {
this.playlist.set(track.name, track);
}
createIterator() {
return new SpotifyPlaylistIterator(this.playlist);
}
}
有了這兩個平台的歌單和反覆器,我們就能輕鬆地用同一套程式播放不同格式的音樂清單。
class PlaylistCollection {
constructor(
private appleMusicPlaylist: AppleMusicPlaylist,
private spotifyPlaylist: SpotifyPlaylist
) {}
play() {
const appleMusicPlaylistIterator = this.appleMusicPlaylist.createIterator();
const spotifyPlaylistIterator = this.spotifyPlaylist.createIterator();
console.log("Switch to Apple Music playlist...");
this._play(appleMusicPlaylistIterator);
console.log("\nSwitch to Spotify playlist...");
this._play(spotifyPlaylistIterator);
}
private _play(iterator: Iterator<ITrack>) {
while (iterator.hasNext()) {
const track = iterator.next()!;
console.log(`Now playing ${track.name} by ${track.artistName}`);
}
}
}
執行結果。
Switch to Apple Music playlist...
Now playing 喜劇 by 星野源
Now playing 桃太郎 by 水曜日のカンパネラ
Now playing エクレア by 岡崎体育
Switch to Spotify playlist...
Now playing 旅路 by 藤井風
Now playing inside you by milet
Now playing HOPE by TENDRE
反覆器模式將遍歷聚合物件的行為封裝成物件,讓客戶端透過反覆器來取得元素,而不需考慮內部的實作細節。這樣做替客戶端省去了手動遍歷資料的麻煩,也避免客戶直接操作資料。
反覆器將元素的取得邏輯從聚合物件中分離出來,減少了聚合物件的職責,使其目的更為清晰。此外,反覆器提供了一個抽象介面,任何物件都能實踐這個介面,使開發者能夠利用多型的概念設計程式。
https://github.com/chengen0612/design-patterns-typescript/blob/main/patterns/behavioral/iterator.ts